iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Software Development

從 C++ 菜鳥到 Low-Latency 勇者:一場分秒必爭的賽局系列 第 14

[Day 14] High-Performance Concurrency: False Sharing Prevention

  • 分享至 

  • xImage
  •  

在現代電腦系統中,CPU 的運算速度不斷提升,multi-core 處理器也已成為主流。為了充分利用硬體資源,軟體通常會採用 concurrency 或 parallelism,讓多個 thread 同時處理不同的工作。然而當多個執行緒共享記憶體時,除了傳統的 data race 與 lock contention 之外,還存在一個較不容易被察覺但可能嚴重影響效能的難題 —— false sharing。

Machine Code 章節已有提到 false sharing,Data-Oriented Design 中也有講解過此觀念。這裡簡單複習一下,flase sharing 就是兩個 thread 修改不同的變數,但這些變數剛好位於同一個 CPU cache line 導致彼此干擾。其並非是程式邏輯上的錯誤,也沒有任何資料競爭,但 CPU cache 的運作方式卻可能使效能大幅下降。因此在設計高效能並行程式時,理解 false sharing 的原理以及如何預防,是非常重要的一環。

1. False Sharing 為什麼會降低效能?

False sharing 的核心問題來自 cache coherence。多核心 CPU 必須確保不同核心看到的資料一致,因此硬體會使用像 MESI 之類的 cache coherence protocol。當某個核心要修改一個 cache line 時,其他核心中相同 cache line 的副本可能必須被標記為無效。這會造成:

  • Cache Miss 增加。
  • 記憶體存取延遲增加。
  • CPU Core 之間的 Cache Coherence 流量增加。
  • 記憶體匯流排或互連架構的壓力增加。
  • 平行程式的 scalability 下降。

因此,當 thread 數量增加時,效能反而可能變差。需要注意的是,false sharing 與 data race 是兩種完全不同的情境。Data race 會造成程式結果不正確;而 false sharing 主要是一個效能瓶頸,通常不會改變執行結果。

2. Padding —— 讓不同執行緒使用不同 Cache Line

為了避免遇到 false sharing,最直接的方法是在不同的共享變數之間加入 padding,使它們位於不同 cache line:

struct Counter {
    alignas(64) long value;
};

Counter counter1;
Counter counter2;

透過 alignas(64),可以讓資料以 64 bytes 對齊,降低兩個頻繁更新的資料落在同一個 cache line 的機會。

也可以使用額外的 padding。如果 long 為 8 bytes,搭配 56 bytes 的 padding,就能讓一個結構接近 64 bytes:

struct Counter {
    long value;
    char padding[56];
};

不過 padding 並非越多越好,過度使用會增加記憶體使用量,甚至降低 cache 的利用率。因此,應根據實際資料結構與硬體特性進行設計。

3. Cache-Line Alignment —— 增加可讀性的好幫手

除了 padding,也可以使用 alignment 技術。這種方式可以讓每一個 Counter 從 cache line 邊界開始配置,有助於避免不同 Counter 共享同一個 cache line

struct alignas(64) Counter {
    std::atomic<long> value;
};

在高效能程式中,對齊記憶體通常比單純增加 padding 更容易表達設計意圖。

4. Thread-Local Storage —— 分離不同執行緒間頻繁存取的資料

另一種有效的方法,是避免不同 thread 頻繁修改同一區域的共享資料。例如每個 thread 都擁有自己的 counter

thread_local long counter = 0;

每個 thread 先在自己的 local storage 中累積結果,最後再由某個 thread 或透過 reduction 機制統一合併。這種設計不只可以避免 false sharing,也可以減少 lock contention 和 synchronization 的成本。

此外,若某些資料會被頻繁寫入而其他資料主要被讀取,可以考慮將它們放在不同的資料結構中。分離資料可以降低不同存取模式互相影響的可能性。

5. False Sharing 與 Atomic 的關係

許多人可能認為,只要使用 std::atomic 就可以解決並行效能問題,然而事實上並非如此。以下方的程式碼為例子,雖然 ab 的操作是 atomic,因此能夠避免某些資料競爭;但如果 ab 位於同一個 cache line,兩個 thread 同時頻繁修改它們時,仍然可能產生 false sharing。

struct Counters {
    std::atomic<int> a;
    std::atomic<int> b;
};

Atomic 解決的是同步與記憶體一致性的語意,而 padding 或 alignment 解決的則是 cache layout。兩者處理的是不同層次的機制,因此在高效能並行程式中,有時需要搭配使用。

False sharing 的困難之處在於,通常無法單純透過程式碼閱讀直接確認。因為其與資料在實際記憶體中的排列方式、CPU cache line 大小,以及 thread 的存取模式有關。因此,實務上通常需要透過 profiling 與 benchmark 進行效能的分析與測試。


上一篇
[Day 13] 進度存檔 | Low-Latency 練功區冒險日誌
下一篇
[Day 15] High-Performance Concurrency: Memory Models & Ordering
系列文
從 C++ 菜鳥到 Low-Latency 勇者:一場分秒必爭的賽局21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言